
package dcjlib;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.*;

import org.apache.http.*;
import org.apache.http.client.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.params.AllClientPNames;
import org.apache.http.entity.StringEntity;
// import org.apache.http.auth.AuthScope;
// import org.apache.http.auth.UsernamePasswordCredentials;
// import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;

import com.google.gson.Gson;

/**
 * 
 * 
 * Java client for DonorsChoose API
 * 
 * @see 
 *
 */
public class DonorsChooseClient
{
	private static final String CHARSET = "UTF-8";
	
	private static final String SEARCH_PATH = null;
	private static final String PROJECT_DONATION_PATH = "/common/json_api.html";
	private static final String TRANSACTION_PATH = "/common/json_api.html";
	private static final String CHALLENGE_IMPACT_STATS = "/common/json_challenge.html"; 
	
	
	private String apiKey;
	private String apiPassword;
	private HttpClient httpClient;

	public DonorsChooseClient(String apiKey, String apiPassword)
	{
		this(apiKey, apiPassword, null);
	}
	
	public DonorsChooseClient(String apiKey, String apiPassword, HttpClient hc)
	{
		this.apiKey = apiKey;
		this.apiPassword = apiPassword;
		this.httpClient = hc;
	}
	
	/**
	 * 
	 * 
	 * @param searchTerms
	 * @param zoneId
	 * 
	 * @return 
	 * 
	 */
	public SearchResult search(List<String> searchTerms, String zoneId)
	{
		List<String> params = new ArrayList<String>();
		
		params.add("APIKey=" + getApiKey());

		if (zoneId != null)
		{
			params.add("zone=" + zoneId);
		}
		
		if ( (searchTerms != null) && (searchTerms.size() > 0) )
		{
			StringBuilder sb = new StringBuilder();
			sb.append("keywords=");
			for (String term : searchTerms)
			{
				sb.append(encode(term));
				sb.append("+");
			}
			params.add(sb.toString());
		}
		
		return get(SearchResult.class, SEARCH_PATH, params);
		
	}
	
	public GivingPageImpactStats getGivingPageImpactStats(String challengeId)
	{
		List<String> params = new ArrayList<String>();
		
		String path = "todo";
		
		return get(GivingPageImpactStats.class, path, params);
	}
	
	public GiftCodeResult purchaseGiftCode(BigDecimal amount)
	{
		if (amount.doubleValue() <= 0.01)
		{
			throw new IllegalArgumentException("amount:" + amount);
		}
	
		// TODO - implement
		
		String path = "todo";
		
		// "action=giftcode"
		
		return post(path, null, GiftCodeResult.class);
	}
	
	public ProjectDonationResult makeProjectDonation(ProjectDonation donation)
	{
		// TODO : implement
		
		// "action=donate"
		
		return post(PROJECT_DONATION_PATH, null, ProjectDonationResult.class);
	}
	
	protected void put(String path)
	{
		put(path, null);
	}
	
	protected void put(String path, Object requestBodyObject)
	{
		HttpPut put = createHttpPut(path);
		
		if (requestBodyObject != null)
		{
			put.setEntity(toJsonEntity(requestBodyObject));
		}
		
		execute(put);
	}
	
	
	protected void post(String path, Object requestBodyObject)
	{
		post(path, requestBodyObject, null);
	}
	
	protected HttpPut createHttpPut(String path)
	{
		HttpPut put = new HttpPut(getUrlForPath(path));
		return put;
	}

	
	protected HttpGet createHttpGet(String path)
	{
		HttpGet get = new HttpGet(this.getUrlForPath(path));
		return get;
	}

	
	
	protected <T> T post(final String path, final Object obj, final Class<T> responseType)
	{
		
		HttpPost post = createHttpPost(path);
		
		if (obj != null)
		{
			post.setEntity(toJsonEntity(obj));
		}
		
		HttpResponse response = execute(post);

		if (responseType == null)
		{
			return null;
		}
		else
		{
			return fromJson(response, responseType);
		}
	}

	protected HttpEntity toJsonEntity(final Object obj)
	{
		
		String json = toJson(obj);
		
		try
		{
			return new JsonEntity(json);
		}
		catch (Exception ex)
		{
			Class clazz = null;
			if (obj != null)
			{
				clazz = obj.getClass();
			}
			throw new RuntimeException("failure, obj class=" + clazz, ex);
		}
		
	}

	protected void checkResponse(HttpRequest request, HttpResponse response)
	{
		boolean hasError = false;
		
		try
		{
			StatusLine status = response.getStatusLine();
			
			int statusCode = status.getStatusCode();
			
		
			if (statusCode == 404)
			{
				hasError = true;
				throw new RuntimeException(status.getReasonPhrase());
			}
			else if ( (statusCode < 200) || (statusCode > 299) )
			{
				hasError = true;
				
				StringBuilder msg = new StringBuilder();
				
				msg.append("statusCode=" + statusCode);
				
				msg.append("\n");
				
				msg.append("method=" + request.getRequestLine().getMethod());
				
				msg.append("\n");
				
				msg.append(request.getRequestLine().getUri());
				
				
				HttpEntity responseEntity = response.getEntity();
				
				try
				{
					String responseBody = EntityUtils.toString(responseEntity);
					msg.append("\n");
					msg.append("responseBody=" + responseBody);
				}
				catch (Exception ignored)
				{
					// ignored
				}

				throw new RuntimeException("unexpected response\n" + msg);
			}
		}
		finally
		{
			if (hasError)
			{
				close(response);
			}
		}
	}

	private void close(final HttpResponse resp)
	{
		if (resp != null)
		{
			try
			{
				close(resp.getEntity());
			}
			catch (Exception ignored)
			{
				// ignored
			}
		}
	}
	
	private void close(final HttpEntity responseEntity)
	{
		if (responseEntity != null)
		{
			try
			{
				responseEntity.consumeContent();
			}
			catch (Exception ignored)
			{
				// ignored
			}
		}
	}

	protected <T> T fromJson(HttpResponse rsp, Class<T> clazz)
	{
		if (clazz == null)
		{
			return null;
		}
		
		if (rsp.getEntity() == null)
		{
			return null;
		}
		
		try
		{
			String responseBody = EntityUtils.toString(rsp.getEntity());
			return fromJson(responseBody, clazz);
		}
		catch (RuntimeException rtex)
		{
			throw rtex;
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
		finally
		{
			close(rsp);
		}
	}
	
	protected HttpResponse execute(HttpRequestBase method)
	{
		try
		{
			method.setHeader(new BasicHeader("Accept", "application/json"));
			HttpResponse rsp = getHttpClient().execute(method);
			checkResponse(method, rsp);
			return rsp;
		}
		catch (RuntimeException rtex)
		{
			throw rtex;
		}
		catch (Exception ex)
		{
			throw new RuntimeException(ex);
		}
	}

	protected HttpClient getHttpClient()
	{
		if (this.httpClient == null)
		{
			this.httpClient = createHttpClient();
		}
		return this.httpClient;
	}
	
	protected HttpClient createHttpClient()
	{
		DefaultHttpClient client = new DefaultHttpClient();
		
		client.getParams().setParameter(AllClientPNames.USER_AGENT, "donorschoose-java-library");

		/*
		CredentialsProvider credProvider = new BasicCredentialsProvider();
		
		credProvider.setCredentials(
		    new AuthScope(getHostname(), AuthScope.ANY_PORT), 
		    new UsernamePasswordCredentials(getUsername(), getPassword()));
		
		client.setCredentialsProvider(credProvider); */
		
		return client;
	}


	private HttpPost createHttpPost(final String path)
	{
		String url = getUrlForPath(path);
		
		HttpPost post = new HttpPost(url);
		
		return post;
	}

	protected String getUrlForPath(final String path)
	{
		if (path.startsWith("http://") || (path.startsWith("https://")))
		{
			return path;
		}
		else
		{
			return "https://" 
				+ getHostname()
				+ path;
		}
	}

	protected String getHostname()
	{
		return "go.urbanairship.com";
	}
	

	protected String getString(final String path, final String... parameters)
	{
		HttpGet get = createHttpGet(path);
		
		HttpResponse rsp = this.execute(get);
		
		return toString(rsp);
	}
	
	protected String toString(HttpResponse rsp)
	{
		try
		{
			return EntityUtils.toString(rsp.getEntity());
		}
		catch (java.io.IOException ex)
		{
			throw new RuntimeException(ex);
		}
		finally
		{
			close(rsp);
		}
	}
	
	
	protected <T> T get(final Class<T> clazz, final String path, final List<String> parameters)
	{
		HttpGet get = createHttpGet(path);
		
		HttpResponse rsp = this.execute(get);
		
		return fromJson(rsp, clazz);
	}
	
	protected <T> String toJson(final T object)
	{
		if (object == null)
		{
			throw new IllegalArgumentException("object parameter is null");
		}
		
		Gson gson = GsonFactory.create();
		return gson.toJson(object);
	}
	
	protected <T> T fromJson(final String json, final Class<T> clazz)
	{
		Gson gson = GsonFactory.create();
		return gson.fromJson(json, clazz);
	}

	protected void delete(final String path)
	{
		HttpDelete delete = createHttpDelete(path);
		execute(delete);
	}

	private HttpDelete createHttpDelete(final String path)
	{
		HttpDelete delete = new HttpDelete(getUrlForPath(path));
		return delete;
	}

	protected String encode(String s)
	{
		try
		{
			return URLEncoder.encode(s, CHARSET);
		}
		catch (UnsupportedEncodingException e)
		{
			throw new RuntimeException(e);
		}
	}

	static private class JsonEntity extends StringEntity
	{
		
		public JsonEntity(String jsonString) throws UnsupportedEncodingException
		{
			super(jsonString, CHARSET);
		}
		
		
		@Override
		public Header getContentType()
		{
			Header h = new BasicHeader("Content-Type", "application/json");
			return h;
		}
		
	}

	public void setUserAgent(String agentString)
	{
		if (agentString == null)
		{
			this.getHttpClient().getParams().removeParameter(AllClientPNames.USER_AGENT);
		}
		else
		{
			this.getHttpClient().getParams().setParameter(AllClientPNames.USER_AGENT, agentString);
		}
	}
	
	public void shutdown()
	{
		if (this.getHttpClient() != null)
		{
			try
			{
				this.getHttpClient().getConnectionManager().shutdown();
			}
			catch (Exception ignored)
			{
				// ignored
			}
		}

	}
	
	protected String getApiKey()
	{
		return this.apiKey;
	}
	
	protected String getApiPassword()
	{
		return this.apiPassword;
	}
}
